
#include "EggNinePCH.h"

#include "EggNineScene.h"
#include "technique.h"
#include "waveTechnique.h"
#include "presentTechnique.h"
#include "CImg.h"

#define _USE_MATH_DEFINES
#include <cstdarg>
#include <cmath>
#include <cassert>
#include <vector>
#include <map>

using namespace cimg_library;

namespace EggNine {

Scene::Scene()
    : m_lights("Lights")
    , m_matrices("Matrices")
    , m_inverseMatrices("InverseMatrices")
    , m_modelStuff("Model")
    , m_material("MaterialAttrs")
    , m_waveParams("WaveSimParams")
    , m_fov((float)M_PI/2)
    , m_near(-.01f)
    , m_far(-100.f)
    , m_qRot(0.0f, 0.0f, 0.0f, 1.0f)
    , m_qRotTheta(0.0f, 0.0f, 0.0f, 1.0f)
    , m_phi(0.0f)
    , m_theta(0.0f)
    , m_lockToY(true)
{
    cimg::temporary_path("assets");
//     glEnable(GL_FRAMEBUFFER_SRGB);
    m_matrices.Camera()=mat4();
    m_matrices.Projection()=mat4();

    for(int i=0;i<MAX_LIGHTS;i++) {
        m_lights.LightColor(i)=vec4();
        m_lightsPos[i]=vec4();
    }
    m_lights.LightColor(0)=_mm_setr_ps(.7f, .7f, .7f, 700.f);
    m_lightsPos[0]=_mm_setr_ps(.0f, 3.5f, -5.f, 1.f);

    // Constants in projection matrix (and inverse)
    m_matrices.Projection()(3,2)=-1;
    m_matrices.Projection()(3,3)=0;
    m_inverseMatrices.Projection()(2,2)=0;
    m_inverseMatrices.Projection()(2,3)=-1;

    m_uniformBlocks[m_lights.Name()]=&m_lights;
    m_uniformBlocks[m_matrices.Name()]=&m_matrices;
    m_uniformBlocks[m_inverseMatrices.Name()]=&m_inverseMatrices;
    m_uniformBlocks[m_modelStuff.Name()]=&m_modelStuff;
    m_uniformBlocks[m_material.Name()]=&m_material;
    m_uniformBlocks[m_waveParams.Name()]=&m_waveParams;

    IProgram* pEffect=new PresentTechnique("simpleShader");
    m_effectMap["SimplePresent"]=pEffect;
    m_drawEffects[PRESENT_SIMPLE]=pEffect;
    m_meshes[PRESENT_SIMPLE].reserve(5);
    pEffect->AttachGlobalUniforms(m_uniformBlocks);

//     pEffect = new WaveSolveTechnique(1024, 1024);
//     m_effectMap["WaveSolve"]=pEffect;
//     pEffect->AttachGlobalUniforms(m_uniformBlocks);

    pEffect = new DisplacedQuadTechnique("tessQuad");
    m_effectMap["TessPresent"]=pEffect;
    m_drawEffects[PRESENT_TESSELATED]=pEffect;
    m_meshes[PRESENT_TESSELATED].reserve(5);
    pEffect->AttachGlobalUniforms(m_uniformBlocks);

    pEffect = new WaveSolveTechnique(4096, 4096);
    m_effectMap["WaveSim"]=pEffect;
    m_simMeshes[SIMULATE_2D].reserve(5);
    m_simulateEffects[SIMULATE_2D]=pEffect;
    pEffect->AttachGlobalUniforms(m_uniformBlocks);

    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}

Scene::~Scene()
{
    for(vector<IMesh*>::iterator it=m_meshList.begin();it!=m_meshList.end();++it)
        delete *it;
    for(ProgramsMap::iterator it=m_effectMap.begin();it!=m_effectMap.end();++it)
        delete it->second;
}

void* Scene::operator new(size_t size)
{
    return _aligned_malloc(size, 16);
}

void Scene::operator delete(void* addr)
{
    _aligned_free(addr);
}

void Scene::Reshape(int width, int height)
{
    m_ar=(float)(width)/height;
    m_width=width;
    m_height=height;

    // View is symmetric, hence projection is simple (by aspect ratio and fov)
    float tmp=tan(m_fov/2);
    mat4& inverseProjection=m_inverseMatrices.Projection();
    inverseProjection(0,0)=tmp;
    inverseProjection(1,1)=tmp/m_ar;
    inverseProjection(3,2)=(m_far-m_near)/(2*m_far*m_near);
    inverseProjection(3,3)=-(m_far+m_near)/(2*m_far*m_near);

    tmp=1/tmp;
    mat4& projection=m_matrices.Projection();
    projection(0,0)=tmp;
    projection(1,1)=m_ar*tmp;
#ifdef ZBUFFER
    projection(2,2)=-(m_far+m_near)/(m_far-m_near);
    projection(2,3)=2.0f*m_far*m_near/(m_far-m_near);
#else
    // WBUFFER
    projection(2,2)=2.0f/(m_far-m_near);
    projection(2,3)=-(m_far+m_near)/(m_far-m_near);
#endif
}

void Scene::Translate(float dx, float dy, float dz)
{
    if(m_lockToY)
        m_Trans-=m_qRotTheta.QConj(vec4(dx,dy,dz,0.0f));
    else
        m_Trans-=vec4(dx,dy,dz,0.0f);
}

void Scene::Scale(float magnitude)
{
    m_fov*=magnitude;

    if(m_fov >= M_PI)
        m_fov=(float)M_PI*.99f;
    Reshape(m_width, m_height);
}

void Scene::RotateAroundAxis(float x, float y, float z, float angle)
{
    // use Rodriguez's rotation formula
    mat4 kx;
    __m128 k;
    __m128i tmp, signs;

    k = _mm_setr_ps(x, y, z, 0);
    signs = _mm_cvtsi32_si128(1<<31);
    tmp = _mm_castps_si128(k);
    
    kx(1) = _mm_castsi128_ps(_mm_xor_si128(_mm_shuffle_epi32(tmp, _MM_SHUFFLE(3, 0, 3, 2)), signs));

    signs = _mm_slli_si128(signs, 4);
    kx(2) = _mm_castsi128_ps(_mm_xor_si128(_mm_shuffle_epi32(tmp, _MM_SHUFFLE(3, 3, 0, 1)), signs));

    signs = _mm_slli_si128(signs, 4);
    kx(0) = _mm_castsi128_ps(_mm_xor_si128(_mm_shuffle_epi32(tmp, _MM_SHUFFLE(3, 1, 2, 3)), signs));

    kx(3) = _mm_setzero_ps();

    kx*=sin(angle);

    mat4 kk;
    kk(0) = _mm_mul_ps(k, _mm_shuffle_ps(k, k, _MM_SHUFFLE(3,0,0,0)));
    kk(1) = _mm_mul_ps(k, _mm_shuffle_ps(k, k, _MM_SHUFFLE(3,1,1,1)));
    kk(2) = _mm_mul_ps(k, _mm_shuffle_ps(k, k, _MM_SHUFFLE(3,2,2,2)));
    kk-=mat4();
    kk*=1-cos(angle);
    kk+=mat4();

    kk+=kx;

    kk.MulRight(m_matrices.Camera())(3,3)=1.f; // This was reset by recent ops
    for(int i=0;i<MAX_LIGHTS;i++)
        kk.MulRight(m_lights.LightPos(i));

    kk-=kx;
    kk-=kx;
    m_inverseMatrices.Camera()*=kk;
    m_inverseMatrices.Camera()(3,3)=1.f;
}

void Scene::CreateCameraMatrix()
{
    m_matrices.Camera()=RotationMatrixFromQ(m_qRot);
    __m128 translate=_mm_or_ps(m_Trans, _mm_setr_ps(0.0f, 0.0f, 0.0f, 1.0f));
    m_matrices.Camera()(3)=translate;
}

void Scene::RotateViewBy( float dphi, float dtheta )
{
    m_phi+=dphi;
    m_theta+=dtheta;
    float a=cos(m_phi*.5f),b=sin(m_phi*.5f);
    float x=cos(m_theta*.5f),y=sin(m_theta*.5f);
    m_qRotTheta=_mm_setr_ps(y,0,0,x);
    __m128 qt=_mm_shuffle_ps(m_qRotTheta, m_qRotTheta, _MM_SHUFFLE(3, 0, 3, 0));    // y, x, y, x
    __m128 qp=_mm_setr_ps(a, b, b, a);

    // The order of application is: T(R(x))
    // hence translation and rotation must first be swapped
    m_Trans=m_qRot.QBackConj(m_Trans);
//     m_qRot=vec4(0,b,0,a);
//     m_qRot.QMulRight(m_qRotTheta);
    m_qRot=_mm_mul_ps(qt, qp);
    m_Trans=m_qRot.QConj(m_Trans);
}

void Scene::Render( float dt )
{
    for(map<MeshSimulateMethod, IProgram*>::iterator it=m_simulateEffects.begin();it!=m_simulateEffects.end();++it) {
        IProgram* pEffect=it->second;

        for(vector<SimulateableMesh*>::iterator jt=m_simMeshes.at(it->first).begin();jt!=m_simMeshes.at(it->first).end();++jt)
            (*jt)->Simulate(dt, pEffect);
    }

//     glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    CreateCameraMatrix();
    m_matrices.Update();
    
    for(int i=0;i<MAX_LIGHTS;i++)
        m_lights.LightPos(i)=m_matrices.Camera()*m_lightsPos[i];
    m_lights.Update();

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0, 0, m_width, m_height);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    for(map<MeshPresentMethod, IProgram*>::iterator it=m_drawEffects.begin();it!=m_drawEffects.end();++it) {
        PresentTechnique* pEffect=static_cast<PresentTechnique*>(it->second) ;

        pEffect->SetViewport(0, 0, m_width, m_height);
        pEffect->ResetToProgram();

        pEffect->DrawPass();

        for(vector<PresentableMesh*>::iterator jt=m_meshes.at(it->first).begin();jt!=m_meshes.at(it->first).end();++jt)
            (*jt)->Draw(pEffect);
    }

    //     glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

void Scene::AttachMesh(IMesh* mesh)
{
    PresentableMesh* ptmp=dynamic_cast<PresentableMesh*>(mesh);
    if(ptmp) {
        m_meshes[ptmp->PresentMethod()].push_back(ptmp);
        ptmp->AttachUniforms(m_uniformBlocks);
        ptmp->MapAttributes(m_effectMap);
    }

    SimulateableMesh* stmp=dynamic_cast<SimulateableMesh*>(mesh);
    if(stmp)
        m_simMeshes[stmp->SimulateMethod()].push_back(stmp);

    m_meshList.push_back(mesh);
}

void Scene::AttachUniformBlock(const UniformBlock* uBlock)
{
    UniformMap::iterator it=m_uniformBlocks.find(uBlock->Name());

    // This is something I'd like to report (assert won't cut it)
    if(it!=m_uniformBlocks.end())
        throw "Uniform block already exists";

    m_uniformBlocks[uBlock->Name()]=uBlock;
}

void Scene::SelectEvent(int x, int y)
{
    // TODO: implement
}

} // EggNine